package pl.com.qapps.datastore.berkeley;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.datastore.Category;
import com.google.appengine.api.datastore.DataTypeUtils;
import com.google.appengine.api.datastore.Email;
import com.google.appengine.api.datastore.GeoPt;
import com.google.appengine.api.datastore.IMHandle;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.Link;
import com.google.appengine.api.datastore.PhoneNumber;
import com.google.appengine.api.datastore.PostalAddress;
import com.google.appengine.api.datastore.Rating;
import com.google.appengine.api.datastore.ShortBlob;
import com.google.appengine.api.users.User;
import com.sleepycat.bind.tuple.TupleOutput;

import static pl.com.qapps.datastore.berkeley.impl.EntityBindings.ENTITY_KEY_BINDING;

class PropertyIndexer {

	private static class Int64Type extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeLong(((Number) value).longValue());
		}

	}

	private static class DoubleType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeDouble((double) value);
		}

	}

	private static class BoolType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeBoolean((boolean) value);
		}
	}

	private static class UserType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			User user = (User) value;
			out.writeString(user.getEmail());
			out.writeBytes(user.getAuthDomain());
		}

	}

	private static class DateType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeLong(((Date) value).getTime() * 1000L);
		}

	}

	private static class LinkType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeBytes(((Link) value).getValue());
		}

	}

	private static class ShortBlobType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.write(((ShortBlob) value).getBytes());
		}

	}

	private static class GeoPtType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			GeoPt geoPt = (GeoPt) value;
			out.writeFloat(geoPt.getLatitude());
			out.writeFloat(geoPt.getLongitude());
		}

	}

	private static class CategoryType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeBytes(((Category) value).getCategory());
		}

	}

	private static class RatingType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeLong(((Rating) value).getRating());
		}
	}

	private static class PhoneNumberType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeBytes(((PhoneNumber) value).getNumber());
		}

	}

	private static class PostalAddressType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeBytes(((PostalAddress) value).getAddress());
		}
	}

	private static class EmailType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeBytes(((Email) value).getEmail());
		}

	}

	private static class IMHandleType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			IMHandle handle = (IMHandle) value;
			out.writeString(handle.getAddress());
			out.writeBytes(handle.getProtocol());
		}

	}

	private static class BlobKeyType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeBytes(((BlobKey) value).getKeyString());
		}

	}

	private static class ByteType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeByte((Byte) value);
		}

	}

	private static class ReferenceType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			ENTITY_KEY_BINDING.objectToEntry((Key) value, out);
		}

	}

	private static class StringType extends TypeSerializer {

		@Override
		void serialize(Object value, TupleOutput out) {
			out.writeBytes((String) value);
		}

	}

	private static abstract class TypeSerializer {

		abstract void serialize(Object value, TupleOutput out);
	}

	private PropertyIndexer() {
	}

	private static Map<Class<?>, TypeSerializer> typeMap = new HashMap<>();

	static {
		typeMap.put(String.class, new StringType());
		typeMap.put(Byte.class, new ByteType());
		typeMap.put(Short.class, new Int64Type());
		typeMap.put(Integer.class, new Int64Type());
		typeMap.put(Long.class, new Int64Type());
		typeMap.put(Float.class, new DoubleType());
		typeMap.put(Double.class, new DoubleType());
		typeMap.put(Boolean.class, new BoolType());
		typeMap.put(User.class, new UserType());
		typeMap.put(Key.class, new ReferenceType());

		// Blob and Text are not indexed
		// typeMap.put(Blob.class, new BlobType());
		// typeMap.put(Text.class, new TextType());

		typeMap.put(Date.class, new DateType());
		typeMap.put(Link.class, new LinkType());
		typeMap.put(ShortBlob.class, new ShortBlobType());
		typeMap.put(GeoPt.class, new GeoPtType());
		typeMap.put(Category.class, new CategoryType());
		typeMap.put(Rating.class, new RatingType());
		typeMap.put(PhoneNumber.class, new PhoneNumberType());
		typeMap.put(PostalAddress.class, new PostalAddressType());
		typeMap.put(Email.class, new EmailType());
		typeMap.put(IMHandle.class, new IMHandleType());
		typeMap.put(BlobKey.class, new BlobKeyType());

		for (Class<?> type : typeMap.keySet()) {
			if (!DataTypeUtils.isSupportedType(type)) {
				System.err.println("WARNING removing TypeSerializer(" + type
						+ ") due to lack of datastore support of this type");
			}
		}
	}

	private static TypeSerializer getType(Class<?> clazz) {
		TypeSerializer type = typeMap.get(clazz);
		if (type == null)
			throw new IllegalArgumentException(clazz + " type is unsupported");
		return type;
	}

	public static void serialize(Object value, TupleOutput out) {
		if (out == null)
			throw new NullPointerException("out cannot be null");
		if (value == null)
			throw new NullPointerException("value cannot be null");
		getType(value.getClass()).serialize(value, out);
	}

	public static byte[] serialize(Object value) {
		TupleOutput out = new TupleOutput();
		serialize(value, out);
		return out.toByteArray();
	}

}
